local base = _G
local dofile = dofile
local mouse = mouse
local keyboard = keyboard
module("gui")

local dir = "Extensions/lua/gui/"
dofile(dir .. "font.lua")
dofile(dir .. "canvas.lua")
dofile(dir .. "string.lua")
dofile(dir .. "keyboard.lua")
dofile(dir .. "object.lua")
dofile(dir .. "container.lua")
dofile(dir .. "vcontainer.lua")

-- load all elements
local objdir = dir .. "objects/"
dofile(objdir .. "arrow.lua")
dofile(objdir .. "button.lua")
dofile(objdir .. "checkbox.lua")
dofile(objdir .. "graph.lua")
dofile(objdir .. "image.lua")
dofile(objdir .. "input.lua")
dofile(objdir .. "label.lua")
dofile(objdir .. "list.lua")
dofile(objdir .. "mcontainer.lua")
dofile(objdir .. "scrollbar.lua")
dofile(objdir .. "slider.lua")
dofile(objdir .. "textbox.lua")
dofile(objdir .. "window.lua")

-- styles and settings
styles = {}
styles['font'] = base.Font() -- default font object
styles['repeat_delay'] = 0.5 -- wait period until repeat is on, in seconds
styles['repeat_rate'] = 0.05 -- keystroke repeat rate, in seconds
styles['cursor_blink'] = 0.5 -- time, in seconds
styles['single_click'] = 0.3 -- time, in seconds
styles['double_click'] = 0.6 -- time, in seconds
styles['touchscreen'] = 'off' -- reports touchscreen taps as clicks
styles['swipe_threshold'] = 25 -- used for touchscreen taps, in pixels
styles['align'] = 'center' -- text alignment
styles['line_spacing'] = 25 -- in pixels
styles['padding'] = 5 -- in pixels
styles['press_offset'] = -1 -- text offset, in pixels
styles['drop_shadow_offset'] = -2 -- text drop shadow offset, in pixels

colors = {}
colors['border'] = base.Color(245, 255, 113)
colors['background'] = base.Color(61, 70, 40)
colors['font'] = base.Color(245, 255, 113)
colors['font_highlight'] = base.BLACK
colors['highlight'] = base.Color(245, 255, 113)
colors['item'] = base.Color(104, 126, 58)
colors['gradient'] = base.Color(245, 255, 113)
colors['drop_shadow'] = base.Color(0, 0, 0)

function create()
  if container then
    return
  end

  if styles['font'] then
    styles['font']:load_system('Verdana', 11)
    styles['line_spacing'] = styles['font']:get_size()*1.5
  end

  -- mouse buffer
  mbuffer = {}

  local v = base.display.viewport
  local w, h = v.width, v.height
  container = VContainer(0, 0, w, h)
  base.display.viewport:add_child(container.node)

  time_mpress = 0
  last_kpress = nil
  time_kpress = 0

  if mouse then
    old_mouse_on_press = mouse.on_press
    old_mouse_on_release = mouse.on_release
    old_mouse_on_move = mouse.on_move
    old_mouse_on_wheelmove = mouse.on_wheelmove
    mouse.on_press = mouse_on_press
    mouse.on_release = mouse_on_release
    mouse.on_move = mouse_on_move
    mouse.on_wheelmove = mouse_on_wheelmove
  end
  if keyboard then
    old_keyboard_on_press = keyboard.on_press
    old_keyboard_on_release = keyboard.on_release
    keyboard.on_press = keyboard_on_press
    keyboard.on_release = keyboard_on_release
  end

  timer = base.Timer()
  timer:start(16, true)
  timer.on_tick = tick
end

function destroy()
  if container == nil then
    return
  end
  base.display.viewport:remove_child(container.node)
  container:destroy()
  container = nil
  mbuffer = nil
  last_kpress = nil
  time_kpress = nil
  styles['font']:unload()
  --styles = nil
  --colors = nil  

  if mouse then
    mouse.on_press = old_mouse_on_press
    mouse.on_release = old_mouse_on_release
    mouse.on_move = old_mouse_on_move
    mouse.on_wheelmove = old_mouse_on_wheelmove
    old_mouse_on_press = nil
    old_mouse_on_release = nil
    old_mouse_on_wheelmove = nil
  end
  if keyboard then
    keyboard.on_press = old_keyboard_on_press
    keyboard.on_release = old_keyboard_on_release
    old_keyboard_on_press = nil
    old_keyboard_on_release = nil
  end

  timer:stop()
  timer = nil
end

function set_style(name, value)
  styles[name] = value
end

function get_style(name)
  return styles[name]
end

function get_styles(...)
  base.assert(styles, "GUI is not initialized")
  local ret = {}
  for i = 1, base.select('#', ...) do
    local arg = base.select(i, ...)
    ret[arg] = styles[arg]
  end
  return ret
end

function merge_styles(src, dest)
  for i, v in base.pairs(src) do
    dest[i] = v
  end
  return dest
end

function set_color(name, value)
  colors[name] = value
end

function get_color(name)
  return colors[name]
end

function get_colors(...)
  local ret = {}
  for i = 1, base.select('#', ...) do
    local arg = base.select(i, ...)
    -- make copies of colors
    if colors[arg] then
      ret[arg] = base.Color(colors[arg])
    end
  end
  return ret
end

function get_clipboard()
  return clipboard
end

function set_cilpboard(c)
  clipboard = c
end

-- keyboard input events
function keyboard_command(keyboard, key)
  if container == nil then
    return
  end
  last_kpress = key
  time_kpress = 0
  local ctrl = keyboard:is_down(base.KEY_LCTRL) or keyboard:is_down(base.KEY_RCTRL)
  local shift = keyboard:is_down(base.KEY_LSHIFT) or keyboard:is_down(base.KEY_RSHIFT)
  local nlock = keyboard:is_toggled(base.KEY_NUMLOCK)

  local command = false
  local char = keyboard:get_char(key)
  if char == nil or char == '' or ctrl == true then
    -- convert numpad keys to non-numpad keys if numlock is off
    if nlock == false then
      local npkey = keyboard:get_npad(key)
      if npkey then
        key = npkey
      end
    end
    command = true
  end
  if command == false then
    container:write(char)
  end
  container:key_command(key, ctrl, shift)
  if old_keyboard_on_press then
    old_keyboard_on_press(keyboard, key)
  end
end

function keyboard_on_press(keyboard, key)
  if container == nil then
    return
  end
  container:key_press(key)
  keyboard_command(keyboard, key)
  if old_keyboard_on_release then
    old_keyboard_on_release(keyboard, key)
  end
end

function keyboard_on_release(keyboard, key)
  if container == nil then
    return
  end
  container:key_release(key)
  if old_keyboard_on_release then
    old_keyboard_on_release(keyboard, key)
  end
end

-- mouse events
function mouse_event(t, x, y, b)
  local e = { type = t, x = x, y = y, button = b }
  base.table.insert(mbuffer, 1, e)
  -- keep the buffer at a reasonable size
  if #mbuffer > 20 then
    base.table.remove(mbuffer)
  end
end

function mouse_on_press(mouse, button)
  if container == nil then
    return
  end

  -- get the mouse position in container coords
  local x, y = get_mouse_position()
  container:mouse_press(button, x, y)

  -- the callback has destroyed the GUI
  if container == nil then
    return
  end
	
  -- check the mouse buffer for canceled drag or double click
  local l1 = mbuffer[1]
  local l2 = mbuffer[2]
  if l1 and l1.type == 'move' then
    if l2 and l2.type == 'press' then
      container:cancel_drag(button, x, y)
      -- the callback has destroyed the GUI
      if container == nil then
        return
      end
    end
  end
  if time_mpress < styles.double_click then
    if styles['touchscreen'] == 'on' then
      container:double_click(button, x, y)
    else
      local l1 = mbuffer[1]
      if l1 and l1.type == 'release' and button == l1.button then
        local l2 = mbuffer[2]
        if l2 and l2.type == 'press' and button == l2.button then
          container:double_click(button, x, y)
        end
      end
    end
    -- the callback has destroyed the GUI
    if container == nil then
      return
    end
  end
  mouse_event('press', x, y, button)

  time_mpress = 0
  if old_mouse_on_press then
    old_mouse_on_press(mouse, button)
  end
end

function mouse_on_release(mouse, button)
  if container == nil then
    return
  end
  
  -- get the mouse position in container coords
  local x, y = get_mouse_position()
  container:mouse_release(button, x, y)
  
  -- the callback has destroyed the GUI
  if container == nil then
    return
  end
  
  if time_mpress < styles['single_click'] then
    if styles['touchscreen'] == 'on' then
      -- ignore fast swipes
      local swipe = false
      local l1 = mbuffer[1]
      if l1 and l1.type == 'move' then
        local dist = l1.x*l1.x + l1.y*l1.y
        if base.math.sqrt(dist) > styles['swipe_threshold'] then
          swipe = true
        end
      end
      if swipe == false then
        container:single_click(button, x, y)
      end
    else
      local l1 = mbuffer[1]
      if l1 and l1.type == 'press' and button == l1.button then
        container:single_click(button, x, y)
      end
    end
    if container == nil then
      return
    end
  end

  -- check for the mouse buffer for canceled drag or end drag
  local l1 = mbuffer[1]
  local l2 = mbuffer[2]
  if l1 and l1.type == 'move' then
    if l2 and l2.type == 'press' then
      if button == l2.button then
        container:end_drag(button, x, y)
      else
        container:cancel_drag(button, x, y)
      end
      -- the callback has destroyed the GUI
      if container == nil then
        return
      end
    end
  end
  mouse_event('release', x, y, button)
  if old_mouse_on_release then
    old_mouse_on_release(mouse, button)
  end
end

function mouse_on_move(mouse, dx, dy)
  if container == nil then
    return
  end

  -- get the mouse position in container coords
  local x, y = get_mouse_position()
  container:mouse_move(x, y, x - dx, y - dy)
  
  -- the callback has destroyed the GUI
  if container == nil then
    return
  end
  
  -- check for the mouse buffer for begin drag or dragging
  local l1 = mbuffer[1]
  if l1 and l1.type == 'press' then
    container:begin_drag(l1.button, x - dx, y + dy) -- todo: changed because of new projection: y - dy)
    -- the callback has destroyed the GUI
    if container == nil then
      return
    end
    container:dragging(l1.button, x, y)
    -- the callback has destroyed the GUI
    if container == nil then
      return
    end
  end
	
  local l2 = mbuffer[2]
  if l1 and l1.type == 'move' then
    if l2 and l2.type == 'press' then
      container:dragging(l2.button, x, y)
      -- the callback has destroyed the GUI
      if container == nil then
        return
      end
    end
  end
  if l1 and l1.type == 'move' then
    l1.x = l1.x + dx
    l1.y = l1.y + dy
  else
    mouse_event('move', dx, dy)
  end
  
  if old_mouse_on_move then
    old_mouse_on_move(mouse, dx, dy)
  end
end

function mouse_on_wheelmove(mouse, z)
  container:wheel_move(z)
  if old_mouse_on_wheelmove then
    old_mouse_on_wheelmove(mouse, z)
  end
end

-- returns the mouse position in default container coords
function get_mouse_position()
  if mouse then
    local w, h = container:get_size()
    local x = mouse.xaxis + w/2
    local y = -(mouse.yaxis - h/2)
    return x, y
  end
end

-- update function
function tick(timer)
  if container == nil then
    return
  end

  -- set the root container size to the default viewport
  local v = base.display.viewport
  local w, h = v.width, v.height
  container:set_size(w, h)

  -- make sure the user doesn't overwrite GUI callbacks
  if mouse then
    if mouse.on_press ~= mouse_on_press then
      old_mouse_on_press = mouse.on_press
      mouse.on_press = mouse_on_press
    end
    if mouse.on_release ~= mouse_on_release then
      old_mouse_on_release = mouse.on_release
      mouse.on_release = old_mouse_on_release
    end
    if mouse.on_move ~= mouse_on_move then
      old_mouse_on_move = mouse.on_move
      mouse.on_move = old_mouse_on_move
    end
    if mouse.on_wheelmove ~= mouse_on_wheelmove then
      old_mouse_on_wheelmove = mouse.on_wheelmove
      mouse.on_wheelmove = old_mouse_on_wheelmove
    end
  end
  if keyboard then
    if keyboard.on_press ~= keyboard_on_press then
      old_keyboard_on_press = keyboard.on_press
      keyboard.on_press = old_keyboard_on_press
    end
    if keyboard.on_release ~= keyboard_on_release then
      old_keyboard_on_release = keyboard.on_release
      keyboard.on_release = old_keyboard_on_release
    end
  end

  -- delta in seconds
  local dt = timer:get_delta_ms()/1000

  -- register repeated keystrokes
  if last_kpress and keyboard:is_down(last_kpress) == true then
    -- key press time
    time_kpress = time_kpress + dt
    if time_kpress > styles.repeat_delay then
      if time_kpress > styles.repeat_delay + styles.repeat_rate then
        local ld = last_kpress
        keyboard_command(keyboard, ld)
        -- the callback has destroyed the GUI
        if container == nil then
          return
        end
        time_kpress = styles.repeat_delay
        last_kpress = ld
      end
    end
  end

  -- mouse press time
  if time_mpress then
    time_mpress = time_mpress + dt
  end
  
  container:update(dt)
end